/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package clasificador.redneuronal.neurona;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Observable;

import utils.Logger;

/*
 * TODO:
 * Hay que hacer un metodo train que utilice las derivadas parciales para 
 * entrenar el NeuronBehaviour subyacente
 */
/**
 *
 * Esta clase implementa una neurona generica de la forma que se utilizan la
 * mayoria de las veces, con una función de activación función de unos pesos.
 */
public abstract class Neurona {

    public Signal<Double> outputSignal;
    public List<Signal<Double>> errorSignals;
    public Signal<Double> errorSignal;
    protected List<Signal<Double>> inputs;
    protected List<Signal<Double>> errors;

    public void invalidate() {
        this.outputUpdated = false;
        this.errorUpdated = false;
        this.outputSignal.invalidate();
        this.errorSignal.invalidate();
        for( Signal<Double> s : this.errorSignals ) {
            s.invalidate();
        }
    }

    public Neurona() {
        this.inputs = new ArrayList<>();
        this.errors = new ArrayList<>();

        this.errorUpdated = false;
        this.outputUpdated = false;

        this.errorValues = null;
        this.inputValues = null;

        this.outputSignal = (new Signal<Double>() {
            @Override
            public void recalculate() {
                recalculateOutputValue();
                this.value = Neurona.this.outputValue;
            }
        });
        this.errorSignal = (new Signal<Double>() {
            @Override
            public void recalculate() {
                recalculateErrorValue();
                this.value = Neurona.this.errorValue;
            }
        });

        this.errorSignals = new ArrayList<>();
    }

    private class BackErrorSignal extends Signal<Double> {

        private int i;

        public BackErrorSignal(int i) {
            this.i = i;
        }
        
        @Override
        public void recalculate() {
            value = errorSignal.getValue() * derivative(i, inputValues, outputSignal.getValue());
        }
    }

    /* 
     * Este bloque de metodos es asimetrico porque todas las salidas siempre
     * van a neuronas, pero las entradas pueden venir de cosas que no lo son.
     */
    public void linkBack(Signal<Double> s) {
        BackErrorSignal sig = new BackErrorSignal(this.errorSignals.size());

        this.inputs.add(s);

        this.errorSignals.add(sig);
    }

    public void linkForward(Signal<Double> s) {
        this.errors.add(s);
    }

    public void linkBack(Neurona n) {
        BackErrorSignal sig = new BackErrorSignal(this.errorSignals.size());

        this.inputs.add(n.outputSignal);
        n.errors.add(sig);

        this.errorSignals.add(sig);
    }

    public void linkForward(Neurona n) {
        n.linkBack(this);
    }
    /* 
     * Esto es feo, pero lo hacemos por no crear un nuevo array cada vez que
     * se recalcula la salida.
     */
    private double[] inputValues;
    private double[] errorValues;

    private void recalculateOutputValue() {
        if (this.inputValues == null) {
            inputValues = new double[this.inputs.size()];
        }
        for (int i = 0; i < inputValues.length; i++) {
            inputValues[i] = this.inputs.get(i).getValue();
        }
        //Logger.info("Inputs: " + Arrays.toString(inputValues));
        //Logger.info("Recalculo de valor de salida con entradas " + Arrays.toString(inputValues));
        this.outputValue = this.activationFunction(inputValues);
        //Logger.info("Resultado: " + outputValue);
    }

    protected double getInputValue(int i) {
        if (!this.outputUpdated) {
            if (this.inputValues == null) {
                inputValues = new double[this.inputs.size()];
            }
            inputValues[i] = this.inputs.get(i).getValue();
        }
        return this.inputValues[i];
    }

    protected double getForwardError(int i) {
        if (!this.errorUpdated) {
            if (this.errorValues == null) {
                errorValues = new double[this.errors.size()];
            }
            errorValues[i] = this.errors.get(i).getValue();
        }
        return this.errorValues[i];
    }

    protected double getBackError(int i) {
        return this.errorSignals.get(i).getValue();
    }

    private void recalculateErrorValue() {
        if (this.errorValues == null) {
            errorValues = new double[this.errors.size()];
        }
        for (int i = 0; i < errorValues.length; i++) {
            //Logger.warning(this.errors);
            //Logger.warning(this.errors.get(i));
            //Logger.warning(this.errors.get(i).getValue());
            errorValues[i] = this.errors.get(i).getValue();
        }
        //Logger.info("Recalculo de valor de error con entradas " + Arrays.toString(errorValues));
        this.errorValue = this.errorFunction(errorValues);
        //Logger.info("Resultado: " + errorValue);
    }

    public int getNInputs() {
        return this.inputs.size();
    }

    public int getNErrors() {
        return this.errors.size();
    }
    private boolean outputUpdated;
    private boolean errorUpdated;
    private Double outputValue;
    private Double errorValue;

    public abstract double errorFunction(double[] errors);

    public abstract double activationFunction(double[] inputs);
    /* Output se da para no tener que recalcularlo. Deber�a ser el resultado de activationFunction(inputs) */

    public abstract double derivative(int i, double[] inputs, double output);

    /* Train calcula el nuevo estado tras el entrenamiento.
     */
    public abstract void train(double learningCoefficient);

    /* Inicializa los datos de la neurona una vez la topologia de la red ya esta creada */
    public abstract void init();
    /* TODO
     public abstract void applyTraining();
     */
}
